I search for solutions in this order: Past Code, Unreal Source, Wiki, BUF, groups.yahoo, google, screaming at monitor. – RegularX

Legacy:Will/Creating A HTTP Server List Tab

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

Creating A HTTP Server List Tab[edit]

This page basically explains the steps I've taken over the last day or so to create a server tab that will pull it's list of servers from a file on a webserver somewhere. It took me a fair while because I'd not looked at server tabs or how they all go together before at all.

Talking to the webserver[edit]

If you want to pull some data from a web server, you have to talk to this server. This is the part that gave me the most greif in figuring it out (mainly that my CR and LF and CRLF variables weren't set – it worked once I'd set them).

This part is going to be a BufferedTCPLink subclass.

You need to declare a few variables first:

Var String ServerAddress; //the server address, ie: www.beyondunreal.com
Var String ServerPath; //The path to connect to, ie: /index.php
Var Int ServerPort; //The port on the server to connect to
Var BYBrowser_Page BYBP; //A link to the browser page itself. Used later, and set by the browser page. 
Var Array<String> LinesA; //This holds the results of your query

Righto. That's sorted. Now onto the function and events.

Function RetrieveServers()
{
	LinesA.Length = 0;
	ResetBuffer();
	Resolve(ServerAddress);
}

This should be fairly self explanatory. It resets the string array, resets the buffer (which also sets the CR, LF, and CRLF variables), and tries to resolve the server address to an IP. If the resolve succeeds, event "Resolved" is fired off, which nicely brings me onto the next one...:

Function Resolved(IpAddr Addr)
{
	Addr.Port = ServerPort;
 
	If (BindPort() == 0)
		Return;
 
	Open(Addr);
}

Basically, this sets the port to be used, attempts to bind a port, and opens a connection to the remote server. When the connection is opened, Event "Opened" is called.

Event Opened()
{
	Local String SendString;
 
	SendString = "GET " $ "http://" $ ServerAddress $ ServerPath $ " HTTP/1.0" $ CRLF $ "Host: " $ ServerAddress $ CRLF $ CRLF;
 
	SendBufferedData(SendString);
}

This function quite simply creates the string to be sent to the server (requesting the document), and then adds it to the send buffer.

Now we're onto the processing of the data that is returned, all handled in these next two functions:

event ReceivedText(string Text)
{
	Local int I;
	SplitIntoLines(Text);
	For (i=0;i<linesa.length;i++)
		{
		BYBP.AddToList(BYBP.IPToServer(linesa[i]));
		}
	BYBP.RefreshFinished();
	LinesA.Length = 0;
}
 
Function SplitIntoLines(String Text)
{
	Local int i;
	Local String Temp;
 
	Text = Mid(Text, InStr(Text, CRLF) + 1); //Split off the headers
 
	Do	{
		Temp = "";
		Temp = Left(Text, InStr(Text, Chr(10)));
 
		If ((Temp != "") && (Temp != Chr(10)) && (Temp != Chr(13)))
			{
			LinesA.Length = LinesA.Length + 1;
			LinesA[LinesA.Length -1] = Temp;
			}
		Text = Mid(Text, InStr(Text, Chr(10)) + 1);
		i++;
		} Until ((InStr(Text, Chr(10)) == -1) || (i > 200)); //put in to stop a massive loop which might happen
}

RecievedText is called when, you guessed it, some text is received. It then calls the SplitIntoLines function, who's job it is to remove the headers (all before one CRLF), and then split the remainder into IP's (Seperarated by LF's). The IP's are all added into the dynamic array, and this is used back in the RecievedText function to add the IP's into the server tab's list. The array is then reset.

And of course, you need the defaultproperties:

DefaultProperties
{
	ServerAddress="byut.net"
	ServerPath="/serverlist2k3.php"
	ServerPort=80
}

The tab itself[edit]

This is probably one of the bits that caused me the LEAST greif. I decided to subclass it from the Favourites tab, because that is closest to what I want it to be.

Only one variable here:

Var BYBrowser_HTTPLink BYB;

That's just a reference to the HTTPLink, which is needed to actually query the HTTP server to get the server list.

InitComponent, I think this is one of the places that gave me the least greif. It's all very simple and straightforward.

function InitComponent(GUIController MyController, GUIComponent MyOwner)
{
	Super(Browser_ServerListPageBase).Initcomponent(MyController, MyOwner);
 
	if(SQC == None ) //needed for pingage, etc
		{
		SQC = PlayerOwner().Level.Spawn(class'ServerQueryClient');
		SQC.OnReceivedPingInfo = MyServersList.MyReceivedPingInfo;
		SQC.OnPingTimeout = MyServersList.MyPingTimeout;
		}
 
	//edit buttons
	GUIButton(GUIPanel(Controls[1]).Controls[1]).Caption = "RE-PING LIST";
	GUIButton(GUIPanel(Controls[1]).Controls[6]).bVisible=False;
	GUIButton(GUIPanel(Controls[1]).Controls[7]).Caption = "REFRESH LIST";
	GUIButton(GUIPanel(Controls[1]).Controls[7]).OnClick = RefreshClick;
	StatusBar.WinWidth=0.8;
 
	RefreshList();
}

Skip out the favoutites class in the Super call, else you'll be just messing around, and server query client is something I left out at first, before I realised it was needed to actually talk to the servers themselves. The rest is just altering the buttons, as the Favourites tab leaves out one, and renames another. I changed them back to what they should be.

Next bit is the RefreshList function, which handles loading and using the HTTP Link class.

Function RefreshList()
{
	ForEach PlayerOwner().Level.AllActors(Class'BYBrowser_HTTPLink', BYB)
		Break;
	if(BYB == None)
		BYB = PlayerOwner().Level.Spawn(class'BYBrowser_HTTPLink');
 
	BYB.BYBP = Self;
 
	MyServersList.Clear(); //Clear the current list
	BYB.RetrieveServers();
}

Basically, all that does is find (or create) the HTTP Link, set it's BYBP value (see the section above), clears the current server list, then calls RetrieveServers on the HTTP Link – which as you'll know initiates the connection, etc.

Function RefreshFinished()
{
	MyServersList.AutoPingServers();
}

This diddy function is just called at the end of an update to force the servers to refresh, re-ping, etc.

The following function was ripped right out of another class, and all it does is turn an IP into a ServerResponseLine which is needed to add it to the list.

Function GameInfo.ServerResponseLine IPToServer(string Address)
{
	local GameInfo.ServerResponseLine S;
	local string ipString, portString;
	local int colonPos, portNum;
 
	// Parse text to find IP and possibly port number
	colonPos = InStr(address, ":");
	if(colonPos < 0)
		{
		ipString = address;
		portNum = 7777;
		}
	else
		{
		ipString = Left(address, colonPos);
		portString = Mid(address, colonPos+1);
		portNum = int(portString);
		}
 
	S.IP = ipString;
	S.Port = portNum;
	S.QueryPort = portNum;
	S.ServerName = "Unknown";
 
	Return S;
}

You'll see the above function called in conjunction with the function below in the HTTP Link class.

Function AddToList(GameInfo.ServerResponseLine S)
{
	MyServersList.MyOnReceivedServer(S);
}

Quite simply, that just adds the server to the list. Nothing more to it.

function PingServer( int listid, ServerQueryClient.EPingCause PingCause, GameInfo.ServerResponseLine s )
{
 
	if( PingCause == PC_Clicked )
		SQC.PingServer( listid, PingCause, s.IP, s.QueryPort, QI_RulesAndPlayers, s );
	else
		SQC.PingServer( listid, PingCause, s.IP, s.QueryPort, QI_Ping, s );
}

PingServer. Pings the server. Simple huh? (this might be left over from the Favourites tab superclass, I'm not sure...).

And the below functions are just set to do nothing, because that's what they should do for this tab.

function bool AddIPClick(GUIComponent Sender)
{
}
 
function SaveFavorites()
{
}

And finally, the defaultproperties, where the caption for this tab is set.

DefaultProperties
{
	PageCaption="BY Servers"
}

Loading the tab[edit]

Although this bit was very simple, I'm still kinda peeved that epic/DE didn't include any way to load the tab without having to use some silly "summon" command. Ah well, since this class is basically one simple function, I'll paste it all below and explain it.

function PostBeginPlay()
{
	local ServerBrowser SB;
	local Browser_ServerListPageFavorites BYServerListPage;
 
	ForEach AllObjects(Class'ServerBrowser',SB)	// Search to see if it is already added, if not, add it.
		{
		If (SB != None)
			{
			BYServerListPage = new(None) class'BYBrowser_Page';
			SB.AddBrowserPage(BYServerListPage);
			Break;
			}
		}
 
	Destroy();
}
 
defaultproperties
{
	bHidden=True
}

When this actor is spawned, it looks for the server browser. If it finds it, it adds my tab. It then kills itself. That simple, nothing more to it.

The whole lot[edit]

Well, for you people who just want the code so they can modify it and change it to suit their needs, I've pasted the whole three classes below.

The HTTP Link:

Class BYBrowser_HTTPLink Extends BufferedTCPLink;
 
Var String ServerAddress;
Var String ServerPath;
Var Int ServerPort;
Var BYBrowser_Page BYBP;
Var Array<String> LinesA;
 
Function RetrieveServers()
{
	LinesA.Length = 0;
	ResetBuffer();
	Resolve(ServerAddress);
}
 
Function Resolved(IpAddr Addr)
{
	Addr.Port = ServerPort;
 
	If (BindPort() == 0)
		Return;
 
	Open(Addr);
}
 
Event Opened()
{
	Local String SendString;
 
	SendString = "GET " $ "http://" $ ServerAddress $ ServerPath $ " HTTP/1.0" $ CRLF $ "Host: " $ ServerAddress $ CRLF $ CRLF;
 
	SendBufferedData(SendString);
}
 
Function Tick (Float TimeDelta)
{
	DoBufferQueueIO();
}
 
event ReceivedText(string Text)
{
	Local int I;
 
	SplitIntoLines(Text);
 
	For (i=0;i<linesa.length;i++)
		{
		BYBP.AddToList(BYBP.IPToServer(linesa[i]));
		}
 
	BYBP.RefreshFinished();
 
	LinesA.Length = 0;
}
 
Function SplitIntoLines(String Text)
{
	Local int i;
	Local String Temp;
 
	Text = Mid(Text, InStr(Text, CRLF) + 1); //Split off the headers
 
	Do	{
		Temp = "";
		Temp = Left(Text, InStr(Text, Chr(10)));
 
		If ((Temp != "") && (Temp != Chr(10)) && (Temp != Chr(13)))
			{
			LinesA.Length = LinesA.Length + 1;
			LinesA[LinesA.Length -1] = Temp;
			}
		Text = Mid(Text, InStr(Text, Chr(10)) + 1);
		i++;
		} Until ((InStr(Text, Chr(10)) == -1) || (i > 50)); //put in to stop a massive loop which might happen
}
 
DefaultProperties
{
	ServerAddress="byut.net"
	ServerPath="/serverlist2k3.php"
	ServerPort=80
}

And now, the server tab:

Class BYBrowser_Page extends Browser_ServerListPageFavorites;
 
Var BYBrowser_HTTPLink BYB;
 
function InitComponent(GUIController MyController, GUIComponent MyOwner)
{
	Super(Browser_ServerListPageBase).Initcomponent(MyController, MyOwner);
 
	if(SQC == None ) //needed for pingage, etc
		{
		SQC = PlayerOwner().Level.Spawn(class'ServerQueryClient');
		SQC.OnReceivedPingInfo = MyServersList.MyReceivedPingInfo;
		SQC.OnPingTimeout = MyServersList.MyPingTimeout;
		}
 
	//edit buttons
	GUIButton(GUIPanel(Controls[1]).Controls[1]).Caption = "RE-PING LIST";
	GUIButton(GUIPanel(Controls[1]).Controls[6]).bVisible=False;
	GUIButton(GUIPanel(Controls[1]).Controls[7]).Caption = "REFRESH LIST";
	GUIButton(GUIPanel(Controls[1]).Controls[7]).OnClick = RefreshClick;
	StatusBar.WinWidth=0.8;
 
	RefreshList();
}
 
Function RefreshList()
{
	ForEach PlayerOwner().Level.AllActors(Class'BYBrowser_HTTPLink', BYB)
		Break;
	if(BYB == None)
		BYB = PlayerOwner().Level.Spawn(class'BYBrowser_HTTPLink');
 
	BYB.BYBP = Self;
 
	MyServersList.Clear(); //Clear the current list
	BYB.RetrieveServers();
}
 
Function RefreshFinished()
{
	MyServersList.AutoPingServers();
}
 
Function GameInfo.ServerResponseLine IPToServer(string Address)
{
	local GameInfo.ServerResponseLine S;
	local string ipString, portString;
	local int colonPos, portNum;
 
	// Parse text to find IP and possibly port number
	colonPos = InStr(address, ":");
	if(colonPos < 0)
		{
		ipString = address;
		portNum = 7777;
		}
	else
		{
		ipString = Left(address, colonPos);
		portString = Mid(address, colonPos+1);
		portNum = int(portString) ;
		}
 
	S.IP = ipString;
	S.Port = portNum;
	S.QueryPort = portNum;
	S.ServerName = "Unknown";
 
	Return S;
}
 
Function AddToList(GameInfo.ServerResponseLine S)
{
	MyServersList.MyOnReceivedServer(S);
}
 
function PingServer( int listid, ServerQueryClient.EPingCause PingCause, GameInfo.ServerResponseLine s )
{
 
	if( PingCause == PC_Clicked )
		SQC.PingServer( listid, PingCause, s.IP, s.QueryPort, QI_RulesAndPlayers, s );
	else
		SQC.PingServer( listid, PingCause, s.IP, s.QueryPort, QI_Ping, s );
}
 
//Functions that I now want to mean nothing
function bool AddIPClick(GUIComponent Sender)
{
}
 
function SaveFavorites()
{
}
 
DefaultProperties
{
	PageCaption="BY Servers"
}

And finally, the loader:

class Load extends Actor;
 
function PostBeginPlay()
{
	local ServerBrowser SB;
	local Browser_ServerListPageFavorites BYServerListPage;
 
	ForEach AllObjects(Class'ServerBrowser',SB) //add the tab.
		{
		If (SB != None)
			{
			BYServerListPage = new(None) class'BYBrowser_Page';
			SB.AddBrowserPage(BYServerListPage);
			Break;
			}
		}
 
	Destroy();
}
 
defaultproperties
{
	bHidden=True
}

And there it is!!

Why??![edit]

The above was all written to create a server browser tab that will pick up servers currently running on Blueyonder games (A GSP which also happens to be part of my ISP). It took me a while to do and figure out, so I figured the wiki may like it.


Will: GEBUS CHRIST!! That's gotta be the biggest amount I've added to the wiki since I wrote the stuff on interactions. Please read through, point out code errors, logic errors, spelling errors, grammar errors, or any kind of errors you want. :)

Will: ARGH GODDAMNS**THEADED*****INGB*STARDS!! :|

I just found out (via Wormbo), that epics latest beta patch (2179) prevents the problem of using custom server tabs as a root for a clientside hack by totally f*cking you up all together. You cant use them AT ALL. F*ck people by not including a way to add tabs automatically, f*ck people by not including a stock tab that can be used to download a server list from a wevserver, then f*ck em all again by removed the possibility for custom server tabs. GG epic. >:| >:| >:|

Mychaeel: Chill. – So, what exactly did they change?

Will: I'm not exactly sure (I dont have the patch), but it now seems they've added those kind of packages that are loaded clientside and not preset serverside into the checks. Even the basic server tab itself will prevent you from joining a server, which is why I am hacked off - because they provide no alternative method of adding your own server tab while destroying the possibility of using one loaded yourself. Could this be a result of the email sent to DrSin about the ability to use tabs like that as a cheat?

Mychaeel: Yes, I think it could. To be honest I had expected that the client purity check worked like that all the time.

What would help might be a way to have auto-creation of server tabs or other objects when the (full-screen) menu is displayed, and giving those objects some sort of notification just before the user leaves the menu to go (back) into the game to give them a chance to auto-destruct before a purity check is done. Or they could "freeze" all objects client-side from packages that aren't present on the server (no Tick, no PostRender, and generally no function calls possible).